﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Drawing;

namespace System.Windows.Forms.Design.Behavior;

public sealed partial class BehaviorService
{
    /// <summary>
    ///  The AdornerWindow is a transparent window that resides ontop of the Designer's Frame. This window is used
    ///  by the BehaviorService to intercept all messages. It also serves as a unified canvas on which to paint Glyphs.
    /// </summary>
    private partial class AdornerWindow : Control
    {
        private readonly BehaviorService _behaviorService;
        private static MouseHook? s_mouseHook;
        private static readonly List<AdornerWindow> s_adornerWindowList = [];

        /// <summary>
        ///  Constructor that parents itself to the Designer Frame and hooks all
        ///  necessary events.
        /// </summary>
        internal AdornerWindow(BehaviorService behaviorService, DesignerFrame designerFrame)
        {
            _behaviorService = behaviorService;
            DesignerFrame = designerFrame;
            Dock = DockStyle.Fill;
            AllowDrop = true;
            Text = "AdornerWindow";
            SetStyle(ControlStyles.Opaque, true);
        }

        /// <summary>
        ///  The key here is to set the appropriate TransparentWindow style.
        /// </summary>
        protected override CreateParams CreateParams
        {
            get
            {
                CreateParams cp = base.CreateParams;
                cp.Style &= ~(int)(WINDOW_STYLE.WS_CLIPCHILDREN | WINDOW_STYLE.WS_CLIPSIBLINGS);
                cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_TRANSPARENT;
                return cp;
            }
        }

        internal bool ProcessingDrag { get; set; }

        /// <summary>
        ///  We'll use CreateHandle as our notification for creating our mouse attacher.
        /// </summary>
        [MemberNotNull(nameof(s_mouseHook))]
        protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);
            s_adornerWindowList.Add(this);
            s_mouseHook ??= new MouseHook();
        }

        /// <summary>
        ///  Unhook and null out our mouseHook.
        /// </summary>
        protected override void OnHandleDestroyed(EventArgs e)
        {
            s_adornerWindowList.Remove(this);

            // Unregister the mouse hook once all adorner windows have been disposed.
            if (s_adornerWindowList.Count == 0 && s_mouseHook is not null)
            {
                s_mouseHook.Dispose();
                s_mouseHook = null;
            }

            base.OnHandleDestroyed(e);
        }

        /// <summary>
        ///  Null out our mouseHook and unhook any events.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing && DesignerFrame is not null)
            {
                DesignerFrame = null!;
            }

            base.Dispose(disposing);
        }

        internal DesignerFrame DesignerFrame { get; private set; }

        /// <summary>
        ///  Returns the display rectangle for the adorner window
        /// </summary>
        internal Rectangle DesignerFrameDisplayRectangle
            => DesignerFrameValid ? DesignerFrame.DisplayRectangle : Rectangle.Empty;

        /// <summary>
        ///  Returns true if the DesignerFrame is created and not being disposed.
        /// </summary>
        internal bool DesignerFrameValid
            => DesignerFrame is not null && !DesignerFrame.IsDisposed && DesignerFrame.IsHandleCreated;

        /// <summary>
        ///  Ultimately called by ControlDesigner when it receives a DragDrop message - here, we'll exit from 'drag mode'.
        /// </summary>
        internal void EndDragNotification() => ProcessingDrag = false;

        /// <summary>
        ///  Invalidates the transparent AdornerWindow by asking the Designer Frame beneath it to invalidate.
        ///  Note the they use of the .Update() call for perf. purposes.
        /// </summary>
        internal void InvalidateAdornerWindow()
        {
            if (DesignerFrameValid)
            {
                DesignerFrame.Invalidate(true);
                DesignerFrame.Update();
            }
        }

        /// <summary>
        ///  Invalidates the transparent AdornerWindow by asking the Designer Frame beneath it to invalidate.
        ///  Note the they use of the .Update() call for perf. purposes.
        /// </summary>
        internal void InvalidateAdornerWindow(Region region)
        {
            if (DesignerFrameValid)
            {
                // Translate for non-zero scroll positions
                Point scrollPosition = DesignerFrame.AutoScrollPosition;
                region.Translate(scrollPosition.X, scrollPosition.Y);

                DesignerFrame.Invalidate(region, true);
                DesignerFrame.Update();
            }
        }

        /// <summary>
        ///  Invalidates the transparent AdornerWindow by asking the Designer Frame beneath it to invalidate.
        ///  Note the they use of the .Update() call for perf. purposes.
        /// </summary>
        internal void InvalidateAdornerWindow(Rectangle rectangle)
        {
            if (DesignerFrameValid)
            {
                // Translate for non-zero scroll positions
                Point scrollPosition = DesignerFrame.AutoScrollPosition;
                rectangle.Offset(scrollPosition.X, scrollPosition.Y);

                DesignerFrame.Invalidate(rectangle, true);
                DesignerFrame.Update();
            }
        }

        /// <summary>
        ///  The AdornerWindow hooks all Drag/Drop notification so  they can be forwarded to the appropriate
        ///  Behavior via the BehaviorService.
        /// </summary>
        protected override void OnDragDrop(DragEventArgs e)
        {
            try
            {
                _behaviorService.OnDragDrop(e);
            }
            finally
            {
                ProcessingDrag = false;
            }
        }

        private static bool IsLocalDrag(DragEventArgs e)
        {
            if (e.Data is DropSourceBehavior.BehaviorDataObject)
            {
                return true;
            }
            else
            {
                // Gets all the data formats and data conversion formats in the data object.
                string[] allFormats = e.Data!.GetFormats();

                for (int i = 0; i < allFormats.Length; i++)
                {
                    if (string.Equals(ToolboxFormat, allFormats[i]))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        ///  The AdornerWindow hooks all Drag/Drop notification so  they can be forwarded to the appropriate
        ///  Behavior via the BehaviorService.
        /// </summary>
        protected override void OnDragEnter(DragEventArgs e)
        {
            ProcessingDrag = true;

            // Determine if this is a local drag, if it is, do normal processing otherwise, force a
            // PropagateHitTest.  We need to force this because the OLE D&D service suspends mouse messages
            // when the drag is not local so the mouse hook never sees them.
            if (!IsLocalDrag(e))
            {
                _behaviorService._validDragArgs = e;
                PInvoke.GetCursorPos(out Point point);
                point = PointToClient(point);
                _behaviorService.PropagateHitTest(point);
            }

            _behaviorService.OnDragEnter(null, e);
        }

        /// <summary>
        ///  The AdornerWindow hooks all Drag/Drop notification so they can be forwarded to the appropriate
        ///  Behavior via the BehaviorService.
        /// </summary>
        protected override void OnDragLeave(EventArgs e)
        {
            // set our dragArgs to null so we know not to send drag enter/leave events when we re-enter the dragging area
            _behaviorService._validDragArgs = null;
            try
            {
                _behaviorService.OnDragLeave(null, e);
            }
            finally
            {
                ProcessingDrag = false;
            }
        }

        /// <summary>
        ///  The AdornerWindow hooks all Drag/Drop notification so they can be forwarded to the appropriate
        ///  Behavior via the BehaviorService.
        /// </summary>
        protected override void OnDragOver(DragEventArgs e)
        {
            ProcessingDrag = true;
            if (!IsLocalDrag(e))
            {
                _behaviorService._validDragArgs = e;
                PInvoke.GetCursorPos(out Point point);
                point = PointToClient(point);
                _behaviorService.PropagateHitTest(point);
            }

            _behaviorService.OnDragOver(e);
        }

        /// <summary>
        ///  The AdornerWindow hooks all Drag/Drop notification so they can be forwarded to the appropriate
        ///  Behavior via the BehaviorService.
        /// </summary>
        protected override void OnGiveFeedback(GiveFeedbackEventArgs e) => _behaviorService.OnGiveFeedback(e);

        /// <summary>
        ///  The AdornerWindow hooks all Drag/Drop notification so they can be forwarded to the appropriate
        ///  Behavior via the BehaviorService.
        /// </summary>
        protected override void OnQueryContinueDrag(QueryContinueDragEventArgs e) => _behaviorService.OnQueryContinueDrag(e);

        /// <summary>
        ///  Called by ControlDesigner when it receives a DragEnter message - we'll let listen to all Mouse
        ///  Messages so we can send drag notifications.
        /// </summary>
        internal void StartDragNotification() => ProcessingDrag = true;

        /// <summary>
        ///  The AdornerWindow intercepts all designer-related messages and forwards them to the BehaviorService
        ///  for appropriate actions.  Note that Paint and HitTest messages are correctly parsed and translated
        ///  to AdornerWindow coords.
        /// </summary>
        protected override unsafe void WndProc(ref Message m)
        {
            // special test hooks
            if (m.Msg == (int)WM_GETALLSNAPLINES)
            {
                _behaviorService.TestHook_GetAllSnapLines(ref m);
            }
            else if (m.Msg == (int)WM_GETRECENTSNAPLINES)
            {
                _behaviorService.TestHook_GetRecentSnapLines(ref m);
            }

            switch (m.MsgInternal)
            {
                case PInvoke.WM_PAINT:
                    {
                        // Stash off the region we have to update.
                        using RegionScope hrgn = new(0, 0, 0, 0);
                        PInvoke.GetUpdateRgn(m.HWND, hrgn, true);

                        // The region we have to update in terms of the smallest rectangle that completely encloses
                        // the update region of the window gives us the clip rectangle.
                        RECT clip = default;
                        PInvoke.GetUpdateRect(m.HWND, &clip, true);
                        Rectangle paintRect = clip;

                        using Region region = hrgn.ToRegion();

                        // Call the base class to do its painting.
                        DefWndProc(ref m);

                        // Now do our own painting.
                        using Graphics g = Graphics.FromHwnd(m.HWnd);
                        using PaintEventArgs pevent = new(g, paintRect);
                        g.Clip = region;
                        _behaviorService.PropagatePaint(pevent);

                        break;
                    }

                case PInvoke.WM_NCHITTEST:
                    Point pt = PARAM.ToPoint(m.LParamInternal);
                    Point pt1 = PointToClient(default);
                    pt.Offset(pt1.X, pt1.Y);

                    m.ResultInternal = _behaviorService.PropagateHitTest(pt) && !ProcessingDrag
                        ? (LRESULT)PInvoke.HTTRANSPARENT
                        : (LRESULT)(int)PInvoke.HTCLIENT;

                    break;

                case PInvoke.WM_CAPTURECHANGED:
                    base.WndProc(ref m);
                    _behaviorService.OnLoseCapture();
                    break;

                default:
                    base.WndProc(ref m);
                    break;
            }
        }

        /// <summary>
        ///  Called by our mouseHook when it spies a mouse message that the adornerWindow would be interested in.
        ///  Returning 'true' signifies that the message was processed and should not continue to child windows.
        /// </summary>
        private bool WndProcProxy(ref Message m, int x, int y)
        {
            Point mouseLoc = new(x, y);
            _behaviorService.PropagateHitTest(mouseLoc);
            switch (m.MsgInternal)
            {
                case PInvoke.WM_LBUTTONDOWN:
                    if (_behaviorService.OnMouseDown(MouseButtons.Left, mouseLoc))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_RBUTTONDOWN:
                    if (_behaviorService.OnMouseDown(MouseButtons.Right, mouseLoc))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_MOUSEMOVE:
                    if (_behaviorService.OnMouseMove(MouseButtons, mouseLoc))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_LBUTTONUP:
                    if (_behaviorService.OnMouseUp(MouseButtons.Left))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_RBUTTONUP:
                    if (_behaviorService.OnMouseUp(MouseButtons.Right))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_MOUSEHOVER:
                    if (_behaviorService.OnMouseHover(mouseLoc))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_LBUTTONDBLCLK:
                    if (_behaviorService.OnMouseDoubleClick(MouseButtons.Left, mouseLoc))
                    {
                        return false;
                    }

                    break;

                case PInvoke.WM_RBUTTONDBLCLK:
                    if (_behaviorService.OnMouseDoubleClick(MouseButtons.Right, mouseLoc))
                    {
                        return false;
                    }

                    break;
            }

            return true;
        }
    }
}
